package pers.whj.net.local.server;

import pers.whj.net.local.exception.ErrorCode;
import pers.whj.net.local.exception.ErrorCodeEnum;
import pers.whj.net.local.exception.WHJException;
import pers.whj.net.local.protocol.Protocol;

import java.io.*;
import java.net.Socket;

public class ServerThread extends Thread {
    protected DataInputStream mInput;
    protected DataOutputStream mOutput;
    protected byte[] mData;
    private final long mID;
    private final Socket mSocket;
    private String mRootPath = "C:\\";
    private String mClientName;
    private EndServerThreadListener mEndServerThreadListener;
    private FileInputStream mFileInput;
    private FileOutputStream mFileOutput;
    private ErrorCode mCurrentErrorCode;
    private File mFile;
    private int mParentLevel;
    private long mFileSize;
    private boolean mEndConnection;

    public ServerThread(Socket socket, long id, String rootPath) {
        mSocket = socket;
        mID = id;
        if (rootPath != null) {
            mRootPath = rootPath;
        }
        mEndConnection = false;
        mCurrentErrorCode = ErrorCodeEnum.UNSPECIFIED;
    }

    public void setEndServerThreadListener(EndServerThreadListener listener) {
        mEndServerThreadListener = listener;
    }

    @Override
    public void run() {
        try {
            if (mSocket != null) {
                mInput = new DataInputStream(mSocket.getInputStream());
                mOutput = new DataOutputStream(mSocket.getOutputStream());
                mData = new byte[Protocol.LENGTH_READ];
                if (initRootPath()) {
                    onConnected();
                    while (!mEndConnection) {
                        mSocket.setSoTimeout(Protocol.TIME_TO_END_CONNECTION);
                        byte command = mInput.readByte();
                        handle(command);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            onDisconnected();
            if (mEndServerThreadListener != null) {
                mEndServerThreadListener.onServerThreadEnd(mID);
            }
            try {
                if (mSocket != null) {
                    mSocket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public long getID() {
        return mID;
    }

    public String getRootPath() {
        return mRootPath;
    }

    public String getClientName() {
        return mClientName;
    }

    protected void handleCommand(byte command) throws IOException {
    }

    protected void onConnected() {
    }

    protected void onDisconnected() {
    }

    protected boolean canCreateRemoteFile() {
        return true;
    }

    protected ErrorCode getCurrentErrorCode() {
        if (mCurrentErrorCode == null) {
            mCurrentErrorCode = ErrorCodeEnum.UNSPECIFIED;
        }
        ErrorCode errorCode = mCurrentErrorCode;
        mCurrentErrorCode = ErrorCodeEnum.UNSPECIFIED;
        return errorCode;
    }

    protected void setCurrentErrorCode(ErrorCode errorCode) {
        mCurrentErrorCode = errorCode;
    }

    protected int checkParentLevel(File file) {
        int parentLevel = 0;
        if (file != null) {
            File parentFile = file.getParentFile();
            while (parentFile != null && !parentFile.exists()) {
                parentLevel++;
                parentFile = parentFile.getParentFile();
            }
        }
        return parentLevel;
    }

    protected void deleteAllFiles(File file, int[] result) {
        if (file != null && file.exists()) {
            result[0]++;
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                if (files != null) {
                    for (File tmp : files) {
                        deleteAllFiles(tmp, result);
                    }
                }
            }
            if (file.delete()) {
                result[1]++;
            }
        }
    }

    protected void deleteEmptyDirectory(File directory, int parentLevel) {
        if (parentLevel > 0 && directory != null && directory.exists() && directory.isDirectory()
                && !directory.getAbsolutePath().equals(mRootPath)
                && directory.getAbsolutePath().length() > mRootPath.length()) {
            parentLevel--;
            String[] list = directory.list();
            if (list != null && list.length == 0 && directory.delete()) {
                deleteEmptyDirectory(directory.getParentFile(), parentLevel);
            }
        }
    }

    protected final byte[] readData(int length) throws IOException {
        byte[] data = new byte[length];
        int receive = 0;
        int maxLength = Protocol.LENGTH_READ;
        while (length > maxLength) {
            readData(data, receive, maxLength);
            receive += maxLength;
            length -= maxLength;
        }
        readData(data, receive, length);
        return data;
    }

    protected final void readData(byte[] data, int offset, int length) throws IOException {
        int receive = 0;
        while (length > 0) {
            int len = mInput.read(data, offset + receive, length);
            receive += len;
            length -= len;
        }
    }

    private boolean initRootPath() throws IOException {
        mSocket.setSoTimeout(Protocol.TIMEOUT_TCP_READ);
        byte[] data = readData(mInput.readInt());
        String info = new String(data, 0, data.length);
        boolean success = false;
        if (info.length() > 0) {
            mClientName = info;
            mRootPath = mRootPath + File.separatorChar + info;
            File file = new File(mRootPath);
            success = (file.exists() && file.isDirectory()) || (!file.exists() && file.mkdirs());
        }
        mOutput.writeByte(success ? Protocol.SUCCESS : Protocol.FAIL);
        return success;
    }

    private void handle(byte command) throws IOException {
        mSocket.setSoTimeout(Protocol.TIMEOUT_TCP_READ);
        int length;
        switch (command) {
            case Protocol.CHECK_STATE:
                mOutput.writeByte(Protocol.CHECK_STATE);
                break;
            case Protocol.END_CONNECTION:
                mEndConnection = true;
                break;
            case Protocol.REQ_UPLOAD:
                length = mInput.readInt();
                boolean uploadSuccess = prepareUploadFile(readData(length));
                mFileSize = mInput.readLong();
                if (uploadSuccess) {
                    mOutput.writeByte(Protocol.RESPONSE);
                    uploadSuccess = uploadFile();
                }
                mOutput.writeByte(uploadSuccess ? Protocol.SUCCESS : Protocol.FAIL);
                if (!uploadSuccess) {
                    mOutput.writeInt(getCurrentErrorCode().getCode());
                }
                endLoadFile();
                break;
            case Protocol.REQ_DOWNLOAD:
                length = mInput.readInt();
                boolean downloadSuccess = prepareDownloadFile(readData(length));
                if (downloadSuccess) {
                    byte[] data = String.valueOf(mFileSize).getBytes();
                    mOutput.writeByte(Protocol.RESPONSE);
                    mOutput.writeInt(data.length);
                    mOutput.write(data, 0, data.length);
                    mOutput.flush();
                    downloadSuccess = downloadFile();
                }
                mOutput.writeByte(downloadSuccess ? Protocol.SUCCESS : Protocol.FAIL);
                if (!downloadSuccess) {
                    mOutput.writeInt(getCurrentErrorCode().getCode());
                }
                endLoadFile();
                break;
            case Protocol.REQ_REMOTE_FILE_INFO:
            case Protocol.REQ_DELETE_REMOTE_FILE:
            case Protocol.REQ_RENAME_REMOTE_FILE:
                byte[] data = readData(mInput.readInt());
                String path = new String(data, 0, data.length);
                mFile = new File(mRootPath + File.separatorChar + path);
                if (mFile.exists()) {
                    switch (command) {
                        case Protocol.REQ_REMOTE_FILE_INFO:
                            mOutput.writeByte(Protocol.RESPONSE);
                            resRemoteFileInfo(mFile, path);
                            mOutput.writeByte(Protocol.DATA_END);
                            break;
                        case Protocol.REQ_DELETE_REMOTE_FILE:
                            int[] result = new int[]{0, 0};
                            deleteAllFiles(mFile, result);
                            if (result[0] == result[1]) {
                                mOutput.writeByte(Protocol.SUCCESS);
                            } else {
                                mOutput.writeByte(Protocol.FAIL);
                                mOutput.writeInt(ErrorCodeEnum.REMOTE_FAIL_DELETE_FILE.getCode());
                            }
                            break;
                        case Protocol.REQ_RENAME_REMOTE_FILE:
                            data = readData(mInput.readInt());
                            String newPath = new String(data, 0, data.length);
                            File newFile = new File(mRootPath + File.separatorChar + newPath);
                            try {
                                if (renameFile(mFile, newFile)) {
                                    mOutput.writeByte(Protocol.SUCCESS);
                                } else {
                                    mOutput.writeByte(Protocol.FAIL);
                                    mOutput.writeInt(ErrorCodeEnum.REMOTE_FAIL_RENAME_FILE.getCode());
                                }
                            } catch (WHJException e) {
                                mOutput.writeByte(Protocol.FAIL);
                                mOutput.writeInt(e.getErrorCode().getCode());
                            }
                            break;
                        default:
                            break;
                    }
                } else {
                    mOutput.writeByte(Protocol.FAIL);
                    mOutput.writeInt(ErrorCodeEnum.REMOTE_FILE_NOT_EXIST.getCode());
                }
                break;
            case Protocol.REQ_CREATE_REMOTE_FILE:
                if (canCreateRemoteFile()) {
                    mOutput.writeByte(Protocol.RESPONSE);
                    createRemoteFile();
                } else {
                    mOutput.writeByte(Protocol.FAIL);
                    mOutput.writeInt(getCurrentErrorCode().getCode());
                }
                break;
            default:
                handleCommand(command);
                break;
        }
    }

    private void resRemoteFileInfo(File file, String currentPath) throws IOException {
        if (file != null && file.exists() && currentPath != null) {
            byte[] data = currentPath.getBytes();
            mOutput.writeByte(Protocol.DATA);
            mOutput.writeInt(data.length);
            mOutput.write(data, 0, data.length);
            mOutput.writeBoolean(file.isFile());
            mOutput.writeLong(file.length());
            mOutput.flush();
            if (file.isDirectory()) {
                File[] files = file.listFiles();
                if (files != null) {
                    String prefix = currentPath + File.separatorChar;
                    for (File tmp : files) {
                        resRemoteFileInfo(tmp, prefix + tmp.getName());
                    }
                }
            }
        }
    }

    private void createRemoteFile() throws IOException {
        File file;
        String filePath;
        boolean isDirectory;
        boolean success;
        int parentLevel = 0;
        while (mInput.readByte() == Protocol.DATA) {
            byte[] data = readData(mInput.readInt());
            isDirectory = mInput.readBoolean();
            filePath = mRootPath + File.separatorChar + new String(data, 0, data.length);
            file = new File(filePath);
            if (file.exists()) {
                success = (file.isDirectory() == isDirectory);
            } else {
                if (isDirectory) {
                    success = file.mkdirs();
                } else {
                    try {
                        parentLevel = checkParentLevel(file);
                        if (parentLevel > 0) {
                            if (!file.getParentFile().mkdirs()) {
                                throw new WHJException(ErrorCodeEnum.REMOTE_FAIL_MAKE_DIRECTORY);
                            }
                        }
                        success = file.createNewFile();
                    } catch (WHJException e) {
                        e.printStackTrace();
                        success = false;
                    } catch (Exception e) {
                        e.printStackTrace();
                        success = false;
                        if (file.exists() && file.delete()) {
                            deleteEmptyDirectory(file.getParentFile(), parentLevel);
                        }
                    }
                }
            }
            mOutput.writeByte(success ? Protocol.SUCCESS : Protocol.FAIL);
        }
    }

    private boolean prepareUploadFile(byte[] data) {
        try {
            String info = new String(data, 0, data.length);
            mFile = new File(mRootPath + File.separatorChar + info);
            mParentLevel = checkParentLevel(mFile);
            if (mParentLevel > 0) {
                if (!mFile.getParentFile().mkdirs()) {
                    setCurrentErrorCode(ErrorCodeEnum.REMOTE_FAIL_MAKE_DIRECTORY);
                    return false;
                }
            }
            if (mFile.isDirectory()) {
                setCurrentErrorCode(ErrorCodeEnum.REMOTE_FILE_IS_DIRECTORY);
                return false;
            }
            if ((!mFile.exists() && mFile.createNewFile()) || mFile.exists()) {
                mFileOutput = new FileOutputStream(mFile);
                return true;
            } else {
                setCurrentErrorCode(ErrorCodeEnum.REMOTE_FAIL_MAKE_FILE);
                return false;
            }
        } catch (Exception e) {
            e.printStackTrace();
            setCurrentErrorCode(ErrorCodeEnum.UNSPECIFIED);
            return false;
        }
    }

    private boolean prepareDownloadFile(byte[] data) {
        try {
            String info = new String(data, 0, data.length);
            mFile = new File(mRootPath + File.separatorChar + info);
            if (!mFile.exists()) {
                setCurrentErrorCode(ErrorCodeEnum.REMOTE_FILE_NOT_EXIST);
                return false;
            }
            if (mFile.isDirectory()) {
                setCurrentErrorCode(ErrorCodeEnum.REMOTE_FILE_IS_DIRECTORY);
                return false;
            }
            mFileInput = new FileInputStream(mFile);
            mFileSize = mFile.length();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            setCurrentErrorCode(ErrorCodeEnum.UNSPECIFIED);
            return false;
        }
    }

    private boolean uploadFile() throws IOException {
        int maxLen = Protocol.LENGTH_READ;
        while (mFileSize > 0) {
            int realLen = (int) Math.min(mFileSize, maxLen);
            try {
                readData(mData, 0, realLen);
            } catch (IOException e) {
                deleteFile(mFile, mParentLevel);
                throw e;
            }
            try {
                mFileOutput.write(mData, 0, realLen);
            } catch (IOException e) {
                e.printStackTrace();
                deleteFile(mFile, mParentLevel);
                setCurrentErrorCode(ErrorCodeEnum.REMOTE_FAIL_WRITE_FILE);
                return false;
            }
            mFileSize -= realLen;
        }
        return true;
    }

    private boolean downloadFile() throws IOException {
        int len;
        while ((len = mFileInput.read(mData, 0, mData.length)) != -1) {
            mOutput.write(mData, 0, len);
            mOutput.flush();
        }
        return true;
    }

    private void endLoadFile() {
        try {
            mFileSize = 0;
            mFile = null;
            if (mFileOutput != null) {
                mFileOutput.close();
            }
            if (mFileInput != null) {
                mFileInput.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private boolean renameFile(File file, File newFile) throws WHJException {
        if (file != null && file.exists() && newFile != null) {
            if (newFile.exists()) {
                throw new WHJException(ErrorCodeEnum.REMOTE_FILE_NAME_EXIST);
            }
            return file.renameTo(newFile);
        }
        return false;
    }

    private boolean deleteFile(File file, int parentLevel) {
        try {
            if (mFileOutput != null) {
                mFileOutput.close();
            }
            if (mFileInput != null) {
                mFileInput.close();
            }
            boolean flag = file != null && file.exists() && file.delete();
            if (flag) {
                deleteEmptyDirectory(file.getParentFile(), parentLevel);
            }
            return flag;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}